import re
from urllib.parse import urlencode
from .segments import Segment, DynamicSegment, Part

__all__ = ('Route',)

class Route:
    def __init__(self, path, name=None, defaults=None, constraints=None):
        self.path = path
        self.name = name
        self.parts = self.parse(path)
        self.dynamic_segments = tuple(s for part in self.parts for s in part.segments if isinstance(s, DynamicSegment))

        constraints = constraints or {}
        for key, c in constraints.items():
            if not isinstance(c, str) or c.startswith('^') or c.endswith('$'):
                raise ValueError(path + ': constraint should be a regex str, not startswith "^" and not endswith "$".')
            segment = self.find_segment(key)
            if not segment: 
                raise ValueError('constraint: {0}: {1} key not exists in path. '.format(key, c))
            segment.constraint = c

        self.constraints = {key: (re.compile(c + '$', re.I).match if isinstance(c, str) else c)
                for key, c in constraints.items()}

        defaults = defaults or {}

        for key, value in defaults.items():
            if value != None and not isinstance(value, str):
                raise ValueError('default value can only be None or a str')
            segment = self.find_segment(key)
            if segment:
                segment.set_default(value)

        self.defaults = defaults

        for part in self.parts:
            part.compile()

        self.verify()

        # keys appear in path but not in defaults and path_segment key
        self.required_keys = [segment.key 
            for part in self.parts 
            for segment in part.segments 
            if isinstance(segment, DynamicSegment) and not segment.optional]

        # keys appear in defaults but not in path
        self.required_default_keys = {k for k, v in defaults.items()} - {segment.key for part in self.parts for segment in part.segments if isinstance(segment, DynamicSegment)}

    @staticmethod
    def parse(path):
        parts = [part for part in path.split('/') if len(part) > 0]
        return tuple(Part(Segment.parse(part)) for part in parts)

    def find_segment(self, key):
        for part in self.parts:
            for segment in part.segments:
                if isinstance(segment, DynamicSegment) and segment.key == key:
                    return segment

    def verify(self):
        non_optionals = [i for i, part in enumerate(self.parts) if not part.optional]
        optionals = [i for i, part in enumerate(self.parts) if part.optional]
        if non_optionals and optionals and optionals[0] < non_optionals[-1]:
            raise ValueError("non-optional part: " + str(self.parts[non_optionals[-1]]) + " behind an optional part." )

        parts_contains_path_segment = [part for part in self.parts if part.contains_path_segment]
        if len(parts_contains_path_segment) > 1:
            raise ValueError("a route can contain only up to one path_segment.")
        elif len(parts_contains_path_segment) == 1 and parts_contains_path_segment[0] != self.parts[-1]:
            raise ValueError("path_segment should be in the last path part.")

    def match(self, path, start=1):
        '''
        path[start:] should be "", "abc", "abc/def", "abc/def/" or "abc/def/..."
        start is possibly >= len(path), for example, according to Mapper.match, 
            if mapper prefix: /app, path: /app, then start will be 5.
        '''
        path_len = len(path)
        assert start >= path_len or path[start] != '/'
                
        if start >= path_len:
            if not self.parts or self.parts[0].optional:
                return dict(self.defaults)
            else:
                return None

        results = {}

        last_part = len(self.parts) - 1
        for i, part in enumerate(self.parts):
            end = path.find('/', start) if not part.contains_path_segment else -1

            m = part.regex.match(path, start, path_len if end == -1 else end)
            if m is None:
                return None
            results.update(m.groupdict())

            if end == -1:
                if i == last_part or self.parts[i + 1].optional:
                    break
                else:
                    return None
            else:
                start = end + 1
        else:
            # all parts are matched, but end != -1
            return None
                
        return dict(self.defaults, **{k: v for k, v in results.items() if v})

    def reverse(self, values, ref_values, check=True):
        '''
        k, v in values: v should be a str and should not be None.
        
        return: "", "abc" or "abc/...", this method doesn't return "/" or any
                str which startswith or endswith "/"
        should not raise any Exception
        '''
        values = dict(values)

        # if ref_values does not match required_default_keys, then ref_values is not usable.
        for key in self.required_default_keys:
            if key not in ref_values or ref_values[key] != self.defaults[key]:
                invalid_ref_values = True
                break
        else:
            invalid_ref_values = False

        # check values against required_default_keys
        for key in self.required_default_keys:
            if key in values:
                if values.pop(key) != self.defaults[key]:
                    return None
            elif invalid_ref_values:
                # if there is a key missing in values, and ref_values is not valid, this route is not usable.
                return None

        if not invalid_ref_values:
            # merge ref_values into values
            for s in self.dynamic_segments:
                key = s.key
                if key not in values:
                    if key in ref_values:
                        values[key] = ref_values[key]
                else:
                    if key in ref_values and values[key] != ref_values[key]:
                        break

        for key in self.required_keys:
            if key not in values:
                return None

        if check:
            for key, c in self.constraints.items():
                # constraints may apply to keys which have default values.
                if key in values and not c(values[key]):
                    return None

        reversed_segments = []
        met_nonempty_segment = False
        for part in reversed(self.parts):
            for s in reversed(part.segments):
                result = s.reverse(values, not met_nonempty_segment)
                if result:
                    met_nonempty_segment = True
                    reversed_segments.append(result)
            if met_nonempty_segment:
                reversed_segments.append('/')

        if reversed_segments:
            reversed_segments.pop() # pop last '/'
        path = ''.join(reversed(reversed_segments))

        if values:
            path += '?' + urlencode(values)

        return path

